/*
 * ============================================================================
 *
 *  Rotoblin
 *
 *  File:			rotoblin.blockscratch.sp
 *  Type:			Module
 *  Description:	Fixes some exploits for the infected, ghost ducking, E spawn expolit, saferoom spawn (nav issue)
 *					and scratching while being staggered.
 *
 *  Copyright (C) 2010  Mr. Zero <mrzerodk@gmail.com>
 *  Copyright (C) 2017-2025  Harry <fbef0102@gmail.com>
 *  This file is part of Rotoblin.
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * ============================================================================
 */

// --------------------
//       Private
// --------------------
#define		EXPOLIT_TIMER			0.5
#define 	DOOR_RANGE_TOLLERANCE 	2500.0

static 			bool: TryToFixClientAmmo[MAXPLAYERS+1]			= {false};

static	const	String:	CLASSNAME_TERRORPLAYER[] 				= "CTerrorPlayer";
static	const	String:	NETPROP_DUCKED[]						= "m_bDucked";
static	const	String:	NETPROP_DUCKING[]						= "m_bDucking";
static	const	String:	NETPROP_FALLVELOCITY[]					= "m_flFallVelocity";

static					g_iOffsetDucked							= -1;
static					g_iOffsetDucking						= -1;
static					g_iOffsetFallVelocity					= -1;

static	const	Float:	TELESPAWN_SPAWN_DELAY					= 0.1;

static					g_iDebugChannel							= 0;
static	const	String:	DEBUG_CHANNEL_NAME[]					= "InfectedExploitFixes";
static g_bTriggerCrouch[MAXPLAYERS+1];
static bool:GhostUseE[MAXPLAYERS+1];
static SpawnClientUse[MAXPLAYERS+1];
static Handle GhostUseE_Timer[MAXPLAYERS+1]	= {null};
static bool:isroundend;
static bool:resuce_start;
static bool:g_bHasLeftSafeRoom;
// **********************************************
//                   Forwards
// **********************************************

/**
 * Plugin is starting.
 *
 * @noreturn
 */
public _InfExloitFixes_OnPluginStart()
{
	g_iOffsetDucked = FindSendPropInfo(CLASSNAME_TERRORPLAYER, NETPROP_DUCKED);
	if (g_iOffsetDucked <= 0) ThrowError("Unable to find ducked offset!");

	g_iOffsetDucking = FindSendPropInfo(CLASSNAME_TERRORPLAYER, NETPROP_DUCKING);
	if (g_iOffsetDucking <= 0) ThrowError("Unable to find ducking offset!");

	g_iOffsetFallVelocity = FindSendPropInfo(CLASSNAME_TERRORPLAYER, NETPROP_FALLVELOCITY);
	if (g_iOffsetFallVelocity <= 0) ThrowError("Unable to find fall velocity offset!");

 	HookPublicEvent(EVENT_ONPLUGINENABLE, _IEF_OnPluginEnable);
	HookPublicEvent(EVENT_ONPLUGINDISABLE, _IEF_OnPluginDisable);
	
	g_iDebugChannel = DebugAddChannel(DEBUG_CHANNEL_NAME);
	DebugPrintToAllEx("Module is now setup");
}

/**
 * Plugin is now enabled.
 *
 * @noreturn
 */
public _IEF_OnPluginEnable()
{
	HookEvent("player_spawn", _IEF_PlayerSpawn_Event);
	HookPublicEvent(EVENT_ONPLAYERRUNCMD, _IEF_OnPlayerRunCmd);
	HookPublicEvent(EVENT_ONCLIENTPUTINSERVER,_IEF_OnClientPutInServer);
	HookPublicEvent(EVENT_ONMAPSTART, _IEF_OnMapStart);
	HookEvent("ammo_pickup", EF_ev_AmmoPickup);
	HookEvent("player_use", EF_ev_PlayerUse);
	HookEvent("player_death", _IEF_Event_PlayerDeath);
	HookEvent("round_start", _IEF_RoundStart_Event, EventHookMode_PostNoCopy);
	HookEvent("round_end", _IEF_Event_RoundEnd, EventHookMode_Pre);
	HookEvent("finale_start", _IEF_Event_Finale_Start);
	HookEvent("finale_radio_start", _IEF_Event_Finale_Start);
		
	CreateTimer(EXPOLIT_TIMER, _EF_t_CheckDuckingExpolit, _, TIMER_REPEAT);
	
	_EF_ToogleHook(true);
}

public _IEF_OnClientPutInServer(client)
{
	//SDKHook(client, SDKHook_PreThinkPost, _IF_SDKh_OnPreThinkPost);
	SDKHook(client, SDKHook_OnTakeDamage, _IF_SDKh_OnTakeDamage);
	
	//SDKHook(client, SDKHook_Touch, SDKHook_IF_OnTouch);
}

public _IEF_Event_PlayerDeath(Handle:event, const String:name[], bool:dontBroadcast)
{
	new client = GetClientOfUserId(GetEventInt(event, "userid"));
	if (g_bHasLeftSafeRoom || !client || !IsClientInGame(client) || !IsFakeClient(client) || GetClientTeam(client) != TEAM_INFECTED) return;
	
	if(L4D_HasAnySurvivorLeftSafeArea() == false)
		CreateTimer(1.5, Kick_Death_AI_Spawn_Timer, client);
	else
		g_bHasLeftSafeRoom = true;
}

public Action:Kick_Death_AI_Spawn_Timer(Handle:timer, any:client)
{
	if (!client || !IsClientInGame(client) || !IsFakeClient(client) || GetClientTeam(client) != TEAM_INFECTED) return;
	
	if(!IsPlayerAlive(client)) 
	{	
		KickClient(client, "Kicked infected ghost bot when survivors are in saferoom");
	}
}

_EF_ToogleHook(bool:bHook)
{
	for (new i = 1; i <= MaxClients; i++){

		if (IsClientInGame(i)){

			if (bHook){

				SDKHook(i, SDKHook_OnTakeDamage, _IF_SDKh_OnTakeDamage);
				//SDKHook(i, SDKHook_Touch, SDKHook_IF_OnTouch);
			}
			else{

				SDKUnhook(i, SDKHook_OnTakeDamage, _IF_SDKh_OnTakeDamage);
				//SDKUnhook(i, SDKHook_Touch, SDKHook_IF_OnTouch);
			}
		}
	}
}

public Action:_IF_SDKh_OnTakeDamage(victim, &attacker, &inflictor, &Float:damage, &damagetype)
{
	if (damagetype & DMG_BULLET && IsClientAndInGame(attacker) && IsIncapacitated(attacker) && GetClientTeam(attacker) == 2 &&
		IsClientAndInGame(victim) && GetClientTeam(victim) == 2) return Plugin_Handled;

	return Plugin_Continue;
}

public Action:_EF_t_CheckDuckingExpolit(Handle:timer)
{
	if (!IsServerProcessing()) return Plugin_Continue;
	if (!IsPluginEnabled()) return Plugin_Stop;

	for (new client = 1; client <= MaxClients; client++){
		if(IsClientConnected(client) && IsClientInGame(client)&& !IsFakeClient(client)){
			if (GetClientTeam(client) == 2 && IsTrueClient(client) && !IsOnLadder(client) && !IsSurvivorBussy(client) && IsUseDuckingExpolit(client)){

				SetEntProp(client, Prop_Send, "m_bDucking", 1);
			}
		
			if (GetClientTeam(client) == 3 && !L4D_IsPlayerStaggering(client) && IsTrueClient(client) && IsPlayerAlive(client) && !IsOnLadder(client) && !IsInfectedBussy(client) && IsUseDuckingExpolit(client)){

			if (IsPlayerGhost(client)) continue;

			SetEntProp(client, Prop_Send, "m_bDucking", 1);
			}
		}
	}

	return Plugin_Continue;
}

bool:IsTrueClient(client)
{
	return !g_bTriggerCrouch[client] && client && IsClientInGame(client);
}

bool:IsUseDuckingExpolit(client)
{
	if (GetEntProp(client, Prop_Send, "m_nDuckTimeMsecs") == 1000)
		return false;

	static iButtons;
	iButtons = GetClientButtons(client);

	if (!(iButtons & IN_DUCK) && !(iButtons & IN_JUMP) && GetEntProp(client, Prop_Send, "m_bDucked") &&
		!GetEntProp(client, Prop_Send, "m_bDucking") && GetEntPropFloat(client, Prop_Send, "m_flFallVelocity") == 0)
		return true;

	return false;
}
/**
 * Plugin is now disabled.
 *
 * @noreturn
 */
public _IEF_OnPluginDisable()
{
	UnhookEvent("player_spawn", _IEF_PlayerSpawn_Event);
	UnhookPublicEvent(EVENT_ONPLAYERRUNCMD, _IEF_OnPlayerRunCmd);
	UnhookPublicEvent(EVENT_ONCLIENTPUTINSERVER, _IEF_OnClientPutInServer);
	UnhookPublicEvent(EVENT_ONMAPSTART, _IEF_OnMapStart);
	UnhookEvent("ammo_pickup", EF_ev_AmmoPickup);
	UnhookEvent("player_use", EF_ev_PlayerUse);	
	UnhookEvent("player_death", _IEF_Event_PlayerDeath);
	UnhookEvent("round_start", _IEF_RoundStart_Event, EventHookMode_PostNoCopy);
	UnhookEvent("round_end", _IEF_Event_RoundEnd, EventHookMode_Pre);
	UnhookEvent("finale_start", _IEF_Event_Finale_Start);
	UnhookEvent("finale_radio_start", _IEF_Event_Finale_Start);
	//UnhookConVarChange(g_hDetectGhostDucking_Cvar, _IEF_DetectGhostDuck_CvarChange);
	
	_EF_ToogleHook(false);
}

public _IEF_OnMapStart()
{
	for (new i = 1; i <= MaxClients; i++)
		SpawnClientUse[i] = 0;
}

public _IEF_RoundStart_Event(Handle:event, const String:name[], bool:dontBroadcast)
{
	g_bHasLeftSafeRoom = false;
	resuce_start = false;
	isroundend = false;
	for (new i = 1; i <= MaxClients; i++)
	{
		GhostUseE[i] = false;
		//ClientSpawnCheck[i] = false;
	}
}

public _IEF_Event_RoundEnd(Handle:event, const String:name[], bool:dontBroadcast)
{
	isroundend = true;
}

public Action:Timer_AmmoCheck(Handle:timer, any:client)
{
	if(IsClientConnected(client)&&IsClientInGame(client)&&GetClientTeam(client) == 2)
	{
		_EF_DoAmmoPilesFix(client,false);
	}
	TryToFixClientAmmo[client] = false;
}

// Fixed up the game mechanics bug when the ammo piles use didn't provide a full ammo refill for weapons.
public EF_ev_AmmoPickup(Handle:event, const String:name[], bool:dontBroadcast)
{
	new client = GetClientOfUserId(GetEventInt(event, "userid"));
	_EF_DoAmmoPilesFix(client,false);
}

public EF_ev_PlayerUse(Handle:event, const String:name[], bool:dontBroadcast)
{
	new target = GetEventInt(event, "targetid");
	if (!IsWeaponSpawnEx(target) || GetEntProp(target, Prop_Send, "m_weaponID") != WEAPID_AMMO) return;

	new client = GetClientOfUserId(GetEventInt(event, "userid"));
	_EF_DoAmmoPilesFix(client, true);
}

public _EF_DoAmmoPilesFix(client, bool:bUse)
{
	decl iEnt;
	if ((iEnt = GetPlayerWeaponSlot(client, 0)) == INVALID_ENT_REFERENCE) return;

	decl String:sClassName[64], iWeapIndex;
	GetEntityClassname(iEnt, sClassName, 64);

	if ((iWeapIndex = GetWeaponIndexByClass(sClassName)) == NULL) return;

	new iClip = GetWeaponClipSize(iEnt);
	
	//PrintToChatAll("iClip: %d - g_iWeapAttributes[iWeapIndex][CLIP_SIZE]: %d - g_iWeapAttributes[iWeapIndex][MAX_AMMO]: %d",iClip,g_iWeapAttributes[iWeapIndex][CLIP_SIZE],g_iWeapAttributes[iWeapIndex][MAX_AMMO]);
		
	if (bUse && (g_iWeapAttributes[iWeapIndex][MAX_AMMO] + g_iWeapAttributes[iWeapIndex][CLIP_SIZE]) == (iClip + GetPrimaryWeaponAmmo(client, iWeapIndex)))
		return;

	SetConVarInt(g_hWeaponCvar[iWeapIndex], g_iWeapAttributes[iWeapIndex][MAX_AMMO] + (g_iWeapAttributes[iWeapIndex][CLIP_SIZE] - iClip));
	CheatCommandEx(client, "give", "ammo");
	SetConVarInt(g_hWeaponCvar[iWeapIndex], g_iWeapAttributes[iWeapIndex][MAX_AMMO]);
}

public _IEF_Event_Finale_Start(Handle:event, const String:name[], bool:dontBroadcast)
{
	if(L4D_IsMissionFinalMap()){
		resuce_start = true;
	}
}

/**
 * Called when a player is spawned.
 *
 * @param event			Handle to event.
 * @param name			String containing the name of the event.
 * @param dontBroadcast	True if event was not broadcast to clients, false otherwise.
 * @noreturn
 */
public _IEF_PlayerSpawn_Event(Handle:event, const String:name[], bool:dontBroadcast)
{
	if (resuce_start) return;

	new client = GetClientOfUserId(GetEventInt(event, "userid"));
	if (!client || IsFakeClient(client) || GetClientTeam(client) != TEAM_INFECTED) return;
	
	if(!isroundend && GhostUseE[client] && GetEntProp(client, Prop_Send, "m_zombieClass") != 5)
	{
		TeleportEntity(client,
			Float:{0.0, 0.0, 0.0}, // Teleport to map center
			NULL_VECTOR, 
			NULL_VECTOR);
			
		decl String:clientName[128];
		GetClientName(client,clientName,128);

		CPrintToChatAll("{default}[{olive}TS{default}] %t","rotoblin22", clientName);	
		ForcePlayerSuicide(client);
		++SpawnClientUse[client];
		if(SpawnClientUse[client]>=2)
		{
			BanClient(client, 360, BANFLAG_AUTHID, "use E+Spawn telespawning glitch", "Nice Try! Uchiha Obito!");
			LogMessage("[Cheater] %N found to be telespawning twice and was banned.", client);
		}
	}
	
	if (!bool:GetEntData(client, g_iOffsetDucked, 1) ||			// If not ducked
		bool:GetEntData(client, g_iOffsetDucking, 1) ||			// or ducking
		(GetClientButtons(client) & IN_DUCK) ||					// or holding down duck
		GetEntDataFloat(client, g_iOffsetFallVelocity) != 0.0)	// or falling
		return;													// return
	
	
	SetEntData(client, g_iOffsetDucked, 0, 1, true); // Unduck player
	DebugPrintToAllEx("Client %i: \"%N\" was ghost ducking and were unducked. Offset status: ducked %b, ducking %b, in duck %b, fall vel %f", 
		client, client, 
		bool:GetEntData(client, g_iOffsetDucked, 1), 
		bool:GetEntData(client, g_iOffsetDucking, 1),
		bool:(GetClientButtons(client) & IN_DUCK),
		GetEntDataFloat(client, g_iOffsetFallVelocity));
}

/**
 * Called when a clients movement buttons are being processed.
 *
 * @param client		Index of the client.
 * @param buttons		Copyback buffer containing the current commands (as bitflags - see entity_prop_stocks.inc).
 * @param impulse		Copyback buffer containing the current impulse command.
 * @param vel			Players desired velocity.
 * @param angles		Players desired view angles.
 * @param weapon		Entity index of the new weapon if player switches weapon, 0 otherwise.
 * @noreturn
 */
public Action:_IEF_OnPlayerRunCmd(client, &buttons, &impulse, Float:vel[3], Float:angles[3], &weapon, &subtype, &cmdnum, &tickcount, &seed, mouse[2])
{
	if(!IsClientInGame(client)) return Plugin_Continue;
	
	if(GetClientTeam(client) == 2 && !TryToFixClientAmmo[client])
	{
		if(buttons & IN_USE)
		{
			if(IsLookingAtAmmo(client))
			{
				TryToFixClientAmmo[client] = true;
				CreateTimer(0.05, Timer_AmmoCheck, client);
				return Plugin_Continue;
			}
		}
	}
	else if(resuce_start == false && GetClientTeam(client) == 3 && IsPlayerGhost(client))
	{
		if(buttons & IN_USE && GhostUseE_Timer[client] == null)
		{
			//PrintToChatAll("%N press E",client);
			
			SetEntProp(client, Prop_Send, "m_ghostSpawnState", L4D_SPAWNFLAG_TOOCLOSE); //Prevent infected from spawning after teleport to survivor
			GhostUseE[client] = true;
			delete GhostUseE_Timer[client];
			GhostUseE_Timer[client] = CreateTimer(TELESPAWN_SPAWN_DELAY, GhostUseE_Time, client);
		}
	}
	return Plugin_Continue;
}

public Action GhostUseE_Time(Handle timer, int client)
{
	//PrintToChatAll("%N press E false",client);
	GhostUseE[client] = false;
	GhostUseE_Timer[client] = null;

	return Plugin_Continue;
}

// **********************************************
//                 Private API
// **********************************************
/**
 * Wrapper for printing a debug message without having to define channel index
 * everytime.
 *
 * @param format		Formatting rules.
 * @param ...			Variable number of format parameters.
 * @noreturn
 */
static DebugPrintToAllEx(const String:format[], any:...)
{
	decl String:buffer[DEBUG_MESSAGE_LENGTH];
	VFormat(buffer, sizeof(buffer), format, 2);
	DebugPrintToAll(g_iDebugChannel, buffer);
}

stock IsLookingAtAmmo(client)
{
	new target = GetClientAimTarget(client, false);
	
	if(target > 0)
	{
		decl String:entName[MAX_NAME_LENGTH];
		
		GetEntityClassname(target, entName, sizeof(entName));
		
		//PrintToChatAll("%N is looking at %s",client,entName);
		if(StrEqual(entName, "weapon_ammo_spawn"))
		{
			return true;
		}
	}
	return false;
}

stock IsTankPlayerInGame(exclude = 0)
{
	for (new i = 1; i <= MaxClients; i++)
		if (exclude != i && IsClientInGame(i) && GetClientTeam(i) == 3 && GetEntProp(i, Prop_Send, "m_zombieClass") == 5 && IsPlayerAlive(i) && !IsIncapacitated(i) && !IsFakeClient(i))
			return true;

	return false;
}
/*
public void _IF_SDKh_OnPreThinkPost(int client)
{
	if (!IsClientInGame(client) ||
		GetClientTeam(client) != TEAM_INFECTED ||
		!IsPlayerGhost(client))
		return;

	int spawnstate = L4D_GetPlayerGhostSpawnState(client);
	//PrintToChatAll("%N (%d) - %d- %d", client, spawnstate, IsPlayerInCheckPoint(client, true), IsPlayerInCheckPoint(client, false));
	if (spawnstate != L4D_SPAWNFLAG_CANSPAWN) return;

	if (!IsPlayerInCheckPoint(client, true) && !IsPlayerInCheckPoint(client, false))
		return;

	L4D_SetPlayerGhostSpawnState(client, spawnstate | L4D_SPAWNFLAG_RESTRICTEDAREA);
}
*/
stock bool IsPlayerInCheckPoint(int client, bool bStartSaferoom)
{
	float pos[3];
	GetEntPropVector(client, Prop_Send, "m_vecOrigin", pos);

	//Address area = L4D_GetLastKnownArea(client);
	Address area = L4D_GetNearestNavArea(pos);
	if (area == Address_Null)
		return false;

	int spawnAttributes = L4D_GetNavArea_SpawnAttributes(area);

	// Some stupid maps like Blood Harvest finale and The Passing finale have CHECKPOINT inside a FINALE marked area.
	// function IsEntityInStartSafeRoom(entity) from https://developer.valvesoftware.com/wiki/L4D2_Vscript_Helpers
	if (spawnAttributes & NAV_SPAWN_FINALE)
	{
		return false;
	}
	else if (spawnAttributes & NAV_SPAWN_CHECKPOINT)
	{
		return bStartSaferoom != L4D2Direct_GetTerrorNavAreaFlow(area) > DOOR_RANGE_TOLLERANCE;
	}

	return false;
}